The Definitive Git Rebase Guide

Dirk Avery
19 min readDec 19, 2018

Okay, maybe I overshot a little with that title. But, whether you want to be a Git ninja, or your boss told you to squash your branch and you have no idea what that means, this guide has you covered.

There are several reasons to rebase. We’ll look at the why and how of two situations that come up a lot:

  1. You’re out of date (i.e., you’re wearing bell-bottom, mom jeans)
  2. You’ve got sketchy commits that might make you look like an idiot

We’ve all been there.

(Photo credit: Shukhrat Umarov)

1. Fixing Out of Date

Imagine you’ve spent a bunch of money customizing a 1969 Dodge Charger. Paint, wheels, bodywork, decals. Then one day you find a ‘71 Charger. You love it. You want it instead. You wish you could take all those customizations you made to the ‘69 and move them to the ‘71.

In the car world that’s not possible. However, in the Git world, developers do this sort of thing everyday. It’s called the fast-forward rebase. You take your customizations to a project, lift them up for a second, slide in another version of the project, and then apply the customizations to that version instead.

Usually we do this so that our changes are applied to the latest and greatest version of a project. We’re catching up — getting up to date. But, you could apply your changes on top of any version you want.

A fast-forward rebase comes up in two common catch-up scenarios:

  1. After development, to catch-up with main project updates since development began
  2. When a Pull Request (PR) has been sitting waiting for a while to be reviewed and merged (but be careful if anyone else depends on the branch, see the Tips for not screwing up a rebase section below)
Before the fast-forward rebase we’re out of date

Before we perform a rebase, our branch with its improvements or bug fixes (i.e., commits 1, 2, and 3), branched off from the main project at a specific point in time (i.e., commit B). Just because we were busily working on great additions to the project does not mean that time stopped. While we worked, the maintainers added commits C, D, and E to the main branch of the project. Now, we are out of date.

The problem with being out of date is that something may have been introduced in C, D, and/or E that either directly conflicts with what we’re doing in our branch (e.g., modifying the exact same line of code in a different way) or that doesn’t play nicely with our branch (e.g., functionality our branch depends on has changed).

The dreaded branch conflict

If we don’t rebase, we won’t notice a conflict until we submit our branch as a Pull Request (PR) and GitHub complains with the message above. Or, there may be no Git conflicts but automated continuous integration (CI) testing might blow up (e.g., Travis-CI) because our code isn’t playing nicely with something new.

Thus, we should test our code on top of the latest, greatest version of the project and not wait until show time (i.e., PR submission) to see it fail.

But, assuming there are no issues, performing the fast-forward rebase is actually very easy. First, we’ll make sure that we’ve got things set up properly. Then, we’ll perform the rebase.

A. Pre-Fast Forward Rebase Setup

These setup steps only have to be done once per repository and then ever after rebases are practically trivial, assuming no conflicts.

To make sure we’re on the same page, I’m assuming that you’ve already done these steps to get to the point where you need to rebase. If not, go here for more guidance.

  1. Forked the main project
  2. Cloned your fork locally
  3. Made a branch for your changes
  4. Then actually made the changes
  5. Committed your changes

To rebase, we need to make sure that we have two remotes: one pointing to our fork and one pointing to the main project. The git remote -v command will give us our remote situation:

$ git remote -v
origin https://github.com/YakDriver/terraform.git (fetch)
origin https://github.com/YakDriver/terraform.git (push)

Here we have one remote called origin, which points to our fork (i.e., YakDriver should be replaced with your GitHub user name). If this is what you see, you’re in good shape.

However, if origin points to the main project or nothing is listed, you need a clone of your own fork. Further guidance can be found here.

Now add a remote to point back to the main project, replacing this example Git URL for the URL of the project you’re working on. We’re going to call this remote upstream, which is convention, but you could call it something else.

$ git remote add upstream https://github.com/hashicorp/terraform.git

That’s it for setup. Now our remotes should look like this:

$ git remote -v
origin https://github.com/YakDriver/terraform.git (fetch)
origin https://github.com/YakDriver/terraform.git (push)
upstream https://github.com/hashicorp/terraform.git (fetch)
upstream https://github.com/hashicorp/terraform.git (push)

B. Perform Fast Forward Rebase

Now to the actual rebase!

We’ll first get the latest, greatest version of the main branch (e.g., master) of the main (i.e., public) project/repository. To do that, we switch to the main branch and then pull (i.e., download) the latest and merge it in.

WARNING: This command will overwrite the local copy of the master branch with the latest upstream remote version of master. This is normally not a problem because we’ve only committed to our branch that won’t be touched. However, any commits that you made locally to master will be overwritten.

We could rebase on a branch besides master. Some projects only accept contributions on the develop branch, for instance. If that’s the case, replace master with develop.

$ git checkout master
$ git pull upstream master
remote: Enumerating objects: 207, done.
remote: Counting objects: 100% (207/207), done.
remote: Compressing objects: 100% (24/24), done.
remote: Total 254 (delta 182), reused 204 (delta 182), pack-reused 47
Receiving objects: 100% (254/254), 160.37 KiB | 14.58 MiB/s, done.
Resolving deltas: 100% (188/188), completed with 121 local objects.
From https://github.com/hashicorp/terraform
* branch master -> FETCH_HEAD
bbd4ede96..2eb0bd4bf master -> upstream/master
Updating 95e44841a..2eb0bd4bf

With the latest version of the main project downloaded, we are ready to switch to our branch (i.e., new-branch) and perform the fast-forward rebase.

If all goes well, this is what the rebase will look like:

$ git checkout new-branch
$ git rebase upstream/master
First, rewinding head to replay your work on top of it...
Applying: Fix the bug (commit #1 summary)
Applying: Fix the other bug (commit #2 summary)
Applying: Add documentation (commit #3 summary)

If, on the other hand, the rebase results in one or more conflicts, you’ll see something like this:

$ git checkout new-branch
$ git rebase upstream/master
First, rewinding head to replay your work on top of it...
Applying: Fix the bug (commit #1 summary)
Using index info to reconstruct a base tree...
M aws/resource_aws_iam_role.go
Falling back to patching base and 3-way merge...
Auto-merging aws/resource_aws_iam_role.go
CONFLICT (content): Merge conflict in aws/resource_aws_iam_role.go
error: Failed to merge in the changes.
Patch failed at 0001 resource/aws_iam_role: Add exclusive lists of policies to role
hint: Use 'git am --show-current-patch' to see the failed patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

This is not the end of the world! See this guidance on fixing it. For this guide, we’ll assume that everything is fixed. Run git rebase --continue to complete the rebase.

At this point, we’ve only performed the rebase locally. We’ll test our code locally with the latest version of the project.

After testing, to upload the new version of the branch to GitHub, we’ll push it. Since we rebased (i.e., rewrote history), Git will complain unless we force the push using -f.

$ git push -f

If we’ve been successful, our branch now looks something like this:

After the fast-forward rebase

2. Fixing Sketchy Commits

The other big reason for rebasing is to fix sketchy, or in other words bad, commits. We all end up with them. Let’s look at how to fix them.

What is a “bad commit?”

What exactly are we talking about here? A “bad commit” is not bad in the sense of it being buggy, non-working code. A rebase won’t fix that. Buggy, non-working code should not be in a commit in the first place.

No, a “bad commit” is either trivial or not well summarized:

  1. Fixing a typo (i.e., a trivial commit)
  2. Changing only spacing (i.e., a trivial commit)
  3. A bunch of changes all attempting to get one thing to work (i.e., trivial commits)
  4. A commit without a good summary (e.g., “asdf” or “Change if statement”)
Some typos are worse than others

Of course, typos are not always trivial and if, for example, you’ve misspelled your company name on the front page of your app, a commit and PR to fix it is absolutely necessary.

However, if you’re submitting a bug fix or an enhancement, changing 10s or 100s of lines of code, a separate commit to fix a typo is trivial and should not be a separate commit.

Here are examples of bad commit messages:

9b20300 Update index.html
e378862 Update index.html
c351b94 Update index.html
7f9ca48 aaa
55d4902 aadf
4902e37 fffd
ab1ca94 asdf

None of these commit messages tells us anything about why these commits exist and are, therefore, bad.

WARNING: Screwing up a rebase can really mess up your repository and your day. You’re rewriting history — history that you depend on to keep all your hard work safe. Follow these tips!

Tips for not screwing up a rebase

  • Never use an IDE to perform rebase (or Git) tasks for you. I’ve learned this lesson the hard way. I’ve had repositories completely mangled by IDEs doing Git. All the Git gurus that I know and trust use command-line Git. Do it!
  • Do not rebase any public or group branches! When you rebase, you’re rewriting the history of a branch. If anyone else depends on that history (e.g., coworkers or the public), it can seriously mess up their branches. If you need to fix problems on a public or group branch, use a commit instead.
  • Make sure that your working tree is clean. Before rebasing, check git status:
$ git status
On branch my-branch
Your branch is up to date with 'origin/my-branch'.
nothing to commit, working tree clean
  • Work on a branch in only one terminal/window. Git maintains per repository locks and internal state that can become inconsistent if you’re modifying things from multiple terminals.
  • Don’t make any changes to files in your branch until your rebase is complete unless you have to, for example, to fix a conflict.
  • Update Git. Before starting a rebase, make sure you have a recent version. If you’re on a Mac, the base MacOS includes an old version of Git. You should not be using it for anything. Use brew install git to get the latest version. If you’re not sure where you stand, check the Git download page to find out what the latest available version is and then check your version:
$ git --version
git version 2.20.1
  • Don’t be afraid to abort if you get in over your head. Abort your failed attempt, learn more and then come back and try again. Aborting puts things back the way they were. There’s no shame in it. Well, maybe a little.
$ git rebase --abort

With the warnings covered, let’s look at four rebase-related skills to have ready to fix bad commits: fixup, amend, squash, and reword. We’ll look at when and how to use each.

Rebase Skill: Fixup

When to use fixup: Use fixup in all sorts of situations to fix what a prior commit does. For example, suppose we added an error message in the original commit. In testing we noticed that a word in the error message is misspelled. Rather than add another commit, we can fixup the original commit. In the end, to the rest of the world, it will look like we never made a mistake in the first place, which, of course, we didn’t.

What fixup does: Git will temporarily add a fixup commit to your branch that fixes the prior commit. When we’re ready, Git will meld any fixup commits into the original commit. (Yes, you can have several fixup commits fixing a single commit.)

Fixup reality: This should be the commit fixing go-to:

  • It’s convenient because you don’t have to go back and try to remember what you meant to do with several commits that may or may not need to be rebased (e.g., which commits did I mean to squash?). At the time you make the change, you’re registering your intent of what should happen and to which commit it should happen.
  • Since fixup commits will get merged into the original commit, we don’t have to worry about tiny, trivial fixes polluting our commit log. We can break up the fixing process into small chunks if we want. If we find 10 things to fix, we can make each one a fixup and then leave it to Git to put everything together correctly later.

How to Fixup

First, make the necessary changes and stage them.

In this example, suppose that we previously did some work on a new enhancement providing a search criteria feature or whatever. We’ve made three commits: one commit for code, one for tests, and one for documentation.

After we made these commits, we noticed a small error in the code that we want to fix. We open VSCode, fix it, and save. Now, as with a normal commit, we’ll stage the change:

$ git add .

But, don’t commit just yet.

Second, figure out which commit we’re fixing.

Use git log to find the SHA abbreviation for the commit (i.e., “commit SHA”) in question:

$ git log --oneline
2eb0bd4 Add documentation for new search criteria
e227bbe Updates tests to cover search criteria
920ac5b Add new search criteria feature
7887937 Merge pull request #6893

We’re fixing a problem in the code, as opposed to the tests or docs, so we’re looking at commit 920ac5b.

Third, commit the change as a fixup to the previous commit.

We’ll use the commit SHA we found in the previous step in the commit command:

$ git commit --fixup 920ac5b

Fourth, find the base of the branch.

If you look at your git log now, you’ll see our new commit with a fixup! prefix followed by the previous commit’s message. Take note of the commit SHA our branch is based on, or in other words, the first public commit or first commit that’s not ours.

$ git log --oneline
335ac23 fixup! Add new search criteria feature
2eb0bd4 Add documentation for new search criteria
e227bbe Updates tests to cover search criteria
920ac5b Add new search criteria feature
7887937 Merge pull request #6893

In our example, the base of our branch is the 7887937 commit.

Fifth, rebase the branch with autosquash.

If we’ve got more fixes, go back to the first step and iterate until everything is good.

When we’re done with all our fixing, with one command we can meld any fixup commits into their parent or parents. Note our use of the branch base commit:

$ git rebase -i --autosquash 7887937

The rebase is interactive (-i) meaning we’ll go back and forth with Git through a short process. After entering the command, Git will open the default editor (e.g., vi on MacOS), showing us the commits in question. We’ll also see helpful comments giving us options.

pick 7887937 Merge pull request #6893
pick 920ac5b Add new search criteria feature
pick e227bbe Updates tests to cover search criteria
pick 2eb0bd4 Add documentation for new search criteria
fixup 335ac23 Add new search criteria feature
# Rebase 7887937..335ac23 onto 7887937 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
...

However, since in a fixup we’ve already done the work of figuring out what we are doing, we can simply quit without changing anything (e.g., q! in vi). Git will perform the rebase and tell us how awesome we are.

Successfully rebased and updated refs/heads/master.
You are super awesome for using a fixup!

Okay. I might have made up that last part.

If we check out our git log now, the fixup will be gone and we’ll have some new SHA hashes. The change made in the fixup is integrated into the previous commit, which is now commit ae8901a:

$ git log --oneline
34adfe2 Add documentation for new search criteria
24a2ed0 Updates tests to cover search criteria
ae8901a Add new search criteria feature
7887937 Merge pull request #6893

Voilà!

At this point, we’ve performed the rebase locally. After testing, to upload the rebased version to GitHub, we’ll push it with -f.

$ git push -f

“Rebase” Skill: Amend

When to use amend: You know you need to make a change to the most recent commit.

What amend does: Git squashes a change into the most recent commit. Unlike fixup, it can do this without us needing to know the commit SHA or the base of the branch.

Note that, technically speaking, since we’re just changing the most recent commit — a leaf, as it were, which is not the base of anything — this isn’t actually a rebase. But, it fills the same role and fits with rebasing nicely.

Amend reality: Amending is quick and easy and fine for a limited application. However, amending is also prone to errors since we are blindly adding a change to what we thought was the most recent commit. If we added a commit more recently and forgot, we could easily change the wrong commit. Using fixup is more precise and less prone to this type of issue.

How to do amend

First, make the necessary changes and stage them.

In this example, like the fixup example, we previously did some work on a new enhancement search criteria feature. We made three commits: one for code, one for tests, and one for documentation.

With amend, we can only make changes to the most recent commit, which happens to be the documentation commit. If we found a typo in the documentation, this would be handy. We make the change, save the file, and then stage the change.

$ git add .

Second, we amend the commit.

Amending the commit, especially when we don’t change the commit message (--no-edit), is easy:

$ git commit --amend --no-edit
[my-branch 2eb0bd4] Add documentation for new search criteria
Date: Tue Dec 18 17:29:08 2018 -0500
1 file changed, 5 insertions(+), 6 deletions(-)

That’s it.

The change has only affected our local repository. After testing, we can upload to GitHub with push. If we already uploaded the old version of the commit to GitHub, we’ll push it with -f.

$ git push -f

Rebase Skill: Squash

When to use squash: Use squash to combine one or more commits with the previous commit. We can use fixup and amend to fix existing commits with new work. However, if we already have all the commits we need but those commits need to be combined, we’ll use squash.

For example, if we added a new welcome message to an app with a typo and then added a commit to fix the typo, we could squash (i.e., meld) those two commits together. We’ll end up with a single, correctly spelled commit.

What squash does: Git takes an existing commit and melds it into the previous commit. Squashes can be chained allowing us to squash a group of commits into the previous commit.

Squash reality: If we’re thinking ahead, we’ll use fixup instead. It’s cleaner and less prone to issues. But, after the fact, when we already have extra commits that don’t need to be on their own, they’re easy to fix with squash.

How to do Squash

First, identify the commits in question.

In this example, suppose we added a commit with a new welcome message for an app. Then, realizing that we’d misspelled the message, we added another commit to fix it. Then, we found another spelling error and committed yet again to fix that error. Now we want to squash these commits all together.

Using git log, we can find the commits.

$ git log --oneline
12ab32a (HEAD) Add ion cannon enhancement
34adfe2 Fix spelling error in welcome message #2
24a2ed0 Fix spelling error in welcome message
ae8901a Add new welcome message
fea12bc Update unrelated feature

Second, request a rebase that goes back far enough.

In the log, we see that the most recent commit, 12ab32a, at the top, is unrelated and should be left alone. This commit, even though it won’t be changed, has to be rebased because it’s going to sit on top of a new melded commit.

Then we have our two spelling fixes, 34adfef and 24a2ed0, and the original welcome message commit, ae8901a. There’s another commit before that, fea12bc, which we don’t care about.

We count down to the earliest commit we care about, ae8901a, and use that count, 4, to begin a rebase in interactive mode (-i).

$ git rebase -i HEAD~4

Git will open the default editor (e.g., vi on MacOS), showing us our sad, misguided commits. Git also gives helpful comments about commands we can use.

NOTE: In this interactive temp file, Git lists the most recent commit last. This can be confusing because when you do a git log, the most recent commit is first. Also, note that Git, on occasion, will reorder your commits seemingly randomly. Git may do this because it’s more efficient to apply the commits that way or because it’s bored. No one knows for sure. Don’t worry though because you can change the order if necessary.

pick ae8901a Add new welcome message
pick 24a2ed0 Fix spelling error in welcome message
pick 34adfe2 Fix spelling error in welcome message #2
pick 12ab32a Add ion cannon enhancement
# Rebase ae8901a..12ab32a onto 12ab32a (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
...

Third, tell Git which commits to squash.

Since we want to squash the third commit into the second, and that commit into the first, we simply replace pick with squash (or the letter s) on the second and third lines. Remember that Git is going to meld a commit into the commit above it in this file.

pick ae8901a Add new welcome message
squash 24a2ed0 Fix spelling error in welcome message
squash 34adfe2 Fix spelling error in welcome message #2
pick 12ab32a Add ion cannon enhancement
# Rebase ae8901a..12ab32a onto 12ab32a (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
...

Save and quit. You should get this message.

Successfully rebased and updated refs/heads/master.

NOTE: The messages associated with squashed commits are tossed out like so many dead roses. Usually this isn’t a problem but if you’re tied to those messages, beware.

If we check out git log at this point, we should see something like this.

$ git log --oneline
113ae33 (HEAD) Add ion cannon enhancement
f45deac Add new welcome message
fea12bc Update unrelated feature

The commit SHAs for the first two commits have changed because they were rebased.

At this point, we’ve performed the rebase locally. After testing, to upload the rebased version to GitHub, we’ll push it with -f.

$ git push -f

Rebase Skill: Reword

When to use reword: Use this when the commit itself (e.g., the code or documentation change) is fine but the commit message (i.e., commit summary) is bad.

What reword does: Rewording does not change how many commits we have or what is in the commits. It only changes commit messages.

Reword reality: This is not used that often. If the message is bad, you probably don’t remember enough later to fix it. And, there are usually deeper problems requiring one of the other rebase weapons. But, if you misspell something in a commit message, it’s the perfect thing.

How to do reword

First, figure out how many commits back we need to go.

Use git log to find the commit with the message that needs to be reworded.

$ git log --oneline
2eb0bd4 (HEAD) Fix name of startup argument
e227bbe Replace the listener with event handler
920ac5b asdf
7887937 Merge pull request #6893

Here, commit 920ac5b, with commit summary “asdf,” is the problem and is 3 lines down.

Second, tell Git we’re rebasing and how many commits to rebase.

The rebase is interactive (-i) meaning we’ll go back and forth with Git through a short process.

We only want to change the message for commit 920ac5b but to get to it we have to include commits 2eb0bd4 and e227bbe, which are more recent. These collateral commits will not be modified by the reword, although they’ll receive new commit SHAs since they’re sitting on a new commit.

$ git rebase -i HEAD~3

Typing this command will open the default editor (e.g., vi on MacOS) showing the commits and a helpful section of comments telling us what we can do:

pick 920ac5b asdf
pick e227bbe Replace the listener with event handler
pick 2eb0bd4 Fix name of startup argument
# Rebase acf5d51..d7ba331 onto acf5d51 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
...

To reword our bad commit message, we change pick to reword (or just r).

reword 920ac5b asdf
pick e227bbe Replace the listener with event handler
pick 2eb0bd4 Fix name of startup argument

Save the file and exit (e.g., using :wq! in vi). Git will open a new default editor with the old commit message, which we can edit.

asdf# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Tue Dec 18 17:29:08 2018 -0500
...

Using standard editing commands, we edit the old message (“asdf”) with something useful about the what and why of the commit.

Update flux capacitor to use Mr. Fusion# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Tue Dec 18 17:29:08 2018 -0500
...

Save and close the editor, and Git will perform the rebase.

Successfully rebased and updated refs/heads/master.

If we check out git log at this point, we should see something like this.

$ git log --oneline
823f671 Fix name of startup argument
edc43ba Replace the listener with event handler
ab34cde Update flux capacitor to use Mr. Fusion
7887937 Merge pull request #6893

The commit SHAs for the first three commits have changed because they were rebased.

At this point, we’ve performed the rebase locally. After testing, to upload the rebased version to GitHub, we’ll push it with -f.

$ git push -f

Whew. Happy rebasing!

--

--

Dirk Avery

Cloud engineer, AI buff, patent attorney, fan of cronuts. AWS Certified Solutions Architect — Professional. Go, Python, automation. https://www.hashicorp.com